package de.justphil.tcg.tcgserver.websockets.services;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Arrays;

import org.slf4j.Logger;

import de.justphil.tcg.tcgserver.commons.domain.containers.NewGame;
import de.justphil.tcg.tcgserver.commons.domain.containers.ObjectHeader;
import de.justphil.tcg.tcgserver.commons.domain.containers.ObjectHeader.Type;
import de.justphil.tcg.tcgserver.websockets.interfaces.TCGameCreator;

import static de.justphil.tcg.tcgserver.commons.logging.TCGLogging.*;

public class GameService extends Thread {
	private static final Logger log = logger(GameService.class);
	
	

	private volatile boolean stop;
	
	private final ServerSocket socket;
	private final int port;
	private final int socketTimeOut;
	
	private volatile Socket clientSocket;
	private volatile ObjectInputStream oin;
	private volatile ObjectOutputStream oout;
	
	private volatile TCGameCreator tcgGameCreator;
	
	public GameService(int port, int socketTimeOut) {
		super(GameService.class.getSimpleName() + "-Thread");
		
		this.port = port;
		this.socketTimeOut = socketTimeOut;
		socket = createServerSocket();
		log.info("ServerSocket created on port {}!", port);
	}
	
	
	
	@Override
	public synchronized void start() {
		stop = false;
		super.start();
	}

	@Override
	public void run() {
		while (!stop) {
			try {
				socket.setSoTimeout(socketTimeOut);
				clientSocket = socket.accept();
				log.info("New service connection accepted on port {}", port);
				
				// First, init the ObjectOutputStream, afterwards init the ObjectInputStream!!!
				// Reason: http://www.jguru.com/faq/view.jsp?EID=333392
				oout = new ObjectOutputStream(clientSocket.getOutputStream());
				/*
				 * JavaDoc 6 SE says:
				 * "[...] callers may wish to flush the stream immediately to ensure that constructors
				 * for receiving ObjectInputStreams will not block when reading the header [...]"
				 */
				oout.flush();
				
				oin = new ObjectInputStream(clientSocket.getInputStream());
				
				while (!Thread.interrupted()) {
					try {
						ObjectHeader oh = (ObjectHeader) oin.readObject();
						handleReceivedObjectHeader(oh, oin, oout);
					}
					catch(InterruptedIOException iioe) {
						log.debug("I was interrupted!");
						this.interrupt();
					}
				}
			}
			catch(SocketTimeoutException ste) {
				log.debug("Still no service connected...");
				continue;
			}
			catch (Exception e) {
				handleError(e);
			}
			finally {
				cleanUp();
			}
		}
	}
	
	public void stopIt() {
		log.debug("stopIt()");
		
		if (stop)
			return;
		
		stop = true;
		
		if (this.isAlive() && !this.isInterrupted()) {
			this.interrupt();
		}
	}
	
	public void sendObject(Object obj) {
		log.debug("sendObject()");
		
		if (stop) {
			log.debug("GameService has been already stopped!");
			return;
		}
		
		if (clientSocket == null) {
			log.debug("No connection to REST server -> sendObject() canceled!");
			return;
		}
		
		synchronized (this) {
			try {
				log.debug("Sending object to REST server...");
				
				oout.writeObject( new ObjectHeader(obj.getClass().getSimpleName(), 0, Type.REQUEST) );
				oout.writeObject(obj);
				oout.flush();
				
				log.debug("Sending object to REST server...DONE!");
			} catch (IOException e) {
				log.error("IOException during writeObject()!");
			}
		}
	}
	
	public void setTCGameCreator(TCGameCreator tcgGameCreator) {
		this.tcgGameCreator = tcgGameCreator;
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	
	private void handleReceivedObjectHeader(ObjectHeader oh, ObjectInputStream ois, ObjectOutputStream oos)
																	throws ClassNotFoundException, IOException {
		if (oh.getClassName().equals(ObjectHeader.NEW_GAME)) {
			handleNewGame(ois, oos, oh.getRequestId());
		}
		else {
			assert false : "Unsupported object header!";
		}
	}
	
	private void handleNewGame(ObjectInputStream ois, ObjectOutputStream oos, long requestId) throws ClassNotFoundException, IOException {
		NewGame newGame = (NewGame) ois.readObject();
		
		// TODO: Kill debug code.
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		log.debug("newGame.getPlayerIds() => {}", newGame.getPlayerIds().toString());
		log.debug("newGame.getDecks()");
		for(int i = 0; i < newGame.getDecks().length; i++) {
			log.debug("newGame.getDecks()[{}] => {}", i, newGame.getDecks()[i].toString());
		}
		log.debug("newGame.getOtps() => {}", Arrays.toString(newGame.getOtps()));
		log.debug("newGame.getPlayerToDeckMapping()");
		for(Long lo : newGame.getPlayerToDeckMapping().keySet()) {
			log.debug("{} => {}", lo, newGame.getPlayerToDeckMapping().get(lo));
		}
		log.debug("newGame.getPlayerToOtpMapping()");
		for(Long lo : newGame.getPlayerToOtpMapping().keySet()) {
			log.debug("{} => {}", lo, newGame.getPlayerToOtpMapping().get(lo));
		}
		/* ######################################################################## */
		/* ######################################################################## */
		/* ######################################################################## */
		
		// Create new game
		if (tcgGameCreator != null) {
			tcgGameCreator.createNewGame(newGame.getGameId(), newGame.getPlayerIds(), newGame.getDecks(), newGame.getOtps(),
					newGame.getPlayerToDeckMapping(), newGame.getPlayerToOtpMapping(), newGame.getInGameCards(), newGame.getGameOtp());
		}
		else {
			log.warn("New game has NOT been created because tcgGameCreator is null!");
		}
		
		// Send response
		synchronized (this) {
			oos.writeObject( new ObjectHeader("null", requestId, Type.RESPONSE) );
			oos.flush();
		}
	}
	
	/* ############################################################################################ */
	/* ############################################################################################ */
	/* ############################################################################################ */
	
	private ServerSocket createServerSocket() {
		try {
			return new ServerSocket(port);
		} catch (IOException e) {
			log.info("Couldn't create ServerSocket on port {}. Shutting down server!", port);
			System.exit(1);
			return null;
		}
	}
	
	private void cleanUp() {
		log.debug("cleanUp()");
		
		try {
			if (oout != null) {
				oout.close();
				oout = null;
			}
			
			if (oin != null) {
				oin.close();
				oin = null;
			}
			
			if (clientSocket != null) {
				clientSocket.close();
				clientSocket = null;
			}
		}
		catch (IOException ioe) {
			handleError(ioe);
		}
	}
	
	private void handleError(Exception e) {
		log.warn("Exception occured: {}", e.getMessage());
	}

}
